本文转自五月的仓颉 https://www.cnblogs.com/xrq730
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看
喜欢的话麻烦点下Star哈
文章将同步到我的个人博客:
本文是微信公众号【Java技术江湖】的《Spring和SpringMVC源码分析》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何从spring基础入手,一步步地学习spring基础和springmvc的框架知识,并上手进行项目实战,spring框架是每一个Java工程师必须要学习和理解的知识点,进一步来说,你还需要掌握spring甚至是springmvc的源码以及实现原理,才能更完整地了解整个spring技术体系,形成自己的知识框架。
后续还会有springboot和springcloud的技术专题,陆续为大家带来,敬请期待。
为了更好地总结和检验你的学习成果,本系列文章也会提供部分知识点对应的面试题以及参考答案。
如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。
spring ioc 容器的加载流程
1.目标:熟练使用spring,并分析其源码,了解其中的思想。这篇主要介绍spring ioc 容器的加载
2.前提条件:会使用debug
3.源码分析方法:Intellj idea debug 模式下源码追溯
通过ClassPathXmlApplicationContext 进行xml 件的读取,从每个堆栈中读取程序的运行信息
4.注意:由于Spring的类继承体系比较复杂,不能全部贴图,所以只将分析源码之后发现的最主要的类继承结构类图贴在下方。
5.关于Spring Ioc
Demo:我们从demo入手一步步进行代码追溯。
Spring Ioc Demo
1.定义数据访问接口IUserDao.java
1 | public interface IUserDao { |
2.定义IUserDao.java实现类IUserDaoImpl.java
1 | public class UserDaoImpl implements IUserDao { |
3.定义业务逻辑接口UserService.java
1 | public interface UserService { |
4.定义UserService.java实现类UserServiceImpl.java
1 | public class UserServiceImpl implements UserService { |
bean.xml配置文件
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
ApplicationContext 继承结构
1.顶层接口:ApplicationContext
2.ClassPathXmlApplicationContext实现类继承AbstractXmlApplication 抽象类
3.AbstractXmlApplication 继承AbstractRefreshableConfigApplicationContext
4.AbstractRefreshableConfigApplicationContext抽象类继承AbstractRefreshableApplicationContext
5.AbstractRefreshableApplicationContext 继承 AbstractApplicationContext
6.AbstractApplicationContext 实现ConfigurableApplicationContext 接口
7.ConfigurableApplicationContext 接口继承
ApplicationContext接口
总体来说继承实现结构较深,内部使用了大量适配器模式。
以ClassPathXmlApplicationContext为例,继承类图如下图所示:
Spring Ioc容器加载过程源码详解
在开始之前,先介绍一个整体的概念。即spring ioc容器的加载,大体上经过以下几个过程:
资源文件定位、解析、注册、实例化
1.资源文件定位
其中资源文件定位,一般是在ApplicationContext的实现类里完成的,因为ApplicationContext接口继承ResourcePatternResolver 接口,ResourcePatternResolver接口继承ResourceLoader接口,ResourceLoader其中的getResource()方法,可以将外部的资源,读取为Resource类。
2.解析DefaultBeanDefinitionDocumentReader,
解析主要是在BeanDefinitionReader中完成的,最常用的实现类是XmlBeanDefinitionReader,其中的loadBeanDefinitions()方法,负责读取Resource,并完成后续的步骤。ApplicationContext完成资源文件定位之后,是将解析工作委托给XmlBeanDefinitionReader来完成的
解析这里涉及到很多步骤,最常见的情况,资源文件来自一个XML配置文件。首先是BeanDefinitionReader,将XML文件读取成w3c的Document文档。
DefaultBeanDefinitionDocumentReader对Document进行进一步解析。然后DefaultBeanDefinitionDocumentReader又委托给BeanDefinitionParserDelegate进行解析。如果是标准的xml namespace元素,会在Delegate内部完成解析,如果是非标准的xml namespace元素,则会委托合适的NamespaceHandler进行解析最终解析的结果都封装为BeanDefinitionHolder,至此解析就算完成。
后续会进行细致讲解。
3.注册
然后bean的注册是在BeanFactory里完成的,BeanFactory接口最常见的一个实现类是DefaultListableBeanFactory,它实现了BeanDefinitionRegistry接口,所以其中的registerBeanDefinition()方法,可以对BeanDefinition进行注册这里附带一提,最常见的XmlWebApplicationContext不是自己持有BeanDefinition的,它继承自AbstractRefreshableApplicationContext,其持有一个DefaultListableBeanFactory的字段,就是用它来保存BeanDefinition
所谓的注册,其实就是将BeanDefinition的name和实例,保存到一个Map中。
刚才说到,最常用的实现DefaultListableBeanFactory,其中的字段就是beanDefinitionMap,是一个ConcurrentHashMap。
代码如下:
>1.DefaultListableBeanFactory继承实现关系
1 | public class DefaultListableBeanFactory |
>2.BeanDefinitionRegistry接口
1 | public interface BeanDefinitionRegistry extends AliasRegistry { |
4.实例化
注册也完成之后,在BeanFactory的getBean()方法之中,会完成初始化,也就是依赖注入的过程
大体上的流程就是这样。
refresh()方法
1.目标:
这篇记录debug 追溯源码的过程,大概分三个篇幅,这是第一篇,现整体了解一下运行流程,定位资源加载,资源解析,bean 注册发生的位置。
2.记录结构:
1.调试栈截图
2.整体流程
3.bean.xml的处理
每段代码下面有相应的讲解
调试栈截图
每个栈帧中方法的行号都有标明,按照行号追溯源码,然后配合教程能够快速学习。
整体流程
ioc容器实例化代码
1 | ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml"); |
进入代码中一步步追溯,发现重要方法:refresh();
如下所示:
1 | public void refresh() throws BeansException, IllegalStateException { |
首先这个方法是同步的,以避免重复刷新。然后刷新的每个步骤,都放在单独的方法里,比较清晰,可以按顺序一个个看
首先是prepareRefresh()方法
1 | protected void prepareRefresh() { |
这个方法里做的事情不多,记录了开始时间,输出日志,另外initPropertySources()方法和validateRequiredProperties()方法一般都没有做什么事。
然后是核心的obtainFreshBeanFactory()方法,这个方法是初始化BeanFactory,是整个refresh()方法的核心,其中完成了配置文件的加载、解析、注册,后面会专门详细说 。
这里要说明一下,ApplicationContext实现了BeanFactory接口,并实现了ResourceLoader、MessageSource等接口,可以认为是增强的BeanFactory。但是ApplicationContext并不自己重复实现BeanFactory定义的方法,而是委托给DefaultListableBeanFactory来实现。这种设计思路也是值得学习的。
后面的 prepareBeanFactory()、postProcessBeanFactory()、invokeBeanFactoryPostProcessors()、registerBeanPostProcessors()、initMessageSource()、initApplicationEventMulticaster()、onRefresh()、registerListeners()、finishBeanFactoryInitialization()、finishRefresh()等方法,是添加一些后处理器、广播、拦截器等,就不一个个细说了
其中的关键方法是finishBeanFactoryInitialization(),在这个方法中,会对刚才注册的Bean(不延迟加载的),进行实例化,所以也是一个核心方法。
bean.xml的处理
从整体上介绍完了流程,接下来就重点看obtainFreshBeanFactory()方法,上文说到,在这个方法里,完成了配置文件的加载、解析、注册
1 | protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { |
这个方法做了2件事,首先通过refreshBeanFactory()方法,创建了DefaultListableBeanFactory的实例,并进行初始化。
1 | protected final void refreshBeanFactory() throws BeansException { |
首先如果已经有BeanFactory实例,就先清空。然后通过createBeanFactory()方法,创建一个DefaultListableBeanFactory的实例
1 | protected DefaultListableBeanFactory createBeanFactory() { |
接下来设置ID唯一标识
1 | beanFactory.setSerializationId(getId()); |
然后允许用户进行一些自定义的配置
1 | protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { |
最后,就是核心的loadBeanDefinitions()方法
1 | protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { |
这里首先会创建一个XmlBeanDefinitionReader的实例,然后进行初始化。这个XmlBeanDefinitionReader中其实传递的BeanDefinitionRegistry类型的实例,为什么可以传递一个beanFactory呢,因为DefaultListableBeanFactory实现了BeanDefinitionRegistry接口,这里是多态的使用。
1 | protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { |
这里要说明一下,ApplicationContext并不自己负责配置文件的加载、解析、注册,而是将这些工作委托给XmlBeanDefinitionReader来做。
1 | loadBeanDefinitions(beanDefinitionReader); |
这行代码,就是Bean定义读取实际发生的地方。这里的工作,主要是XmlBeanDefinitionReader来完成的,下一篇博客会详细介绍这个过程。
loadBeanDefinitions
loadBeanDefinitions: 源码阅读
入口是loadBeanDefinitions方法
1 | protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) |
这是解析过程最外围的代码,首先要获取到配置文件的路径,这在之前已经完成了。
然后将每个配置文件的路径,作为参数传给BeanDefinitionReader的loadBeanDefinitions方法里
1 | public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException { |
这个方法又调用了重载方法
1 | public int loadBeanDefinitions(String location, Set<Resource> actualResources) |
首先getResourceLoader()的实现的前提条件是因为XmlBeanDefinitionReader在实例化的时候已经确定了创建了实例ResourceLoader实例, 代码位于 AbstractBeanDefinitionReader
1 | protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) { |
这个方法比较长,BeanDefinitionReader不能直接加载配置文件,需要把配置文件封装成Resource,然后才能调用重载方法loadBeanDefinitions()。所以这个方法其实就是2段,第一部分是委托ResourceLoader将配置文件封装成Resource,第二部分是调用loadBeanDefinitions(),对Resource进行解析
而这里的ResourceLoader,就是前面的XmlWebApplicationContext,因为ApplicationContext接口,是继承自ResourceLoader接口的
Resource也是一个接口体系,在web环境下,这里就是ServletContextResource
接下来进入重载方法loadBeanDefinitions()
1 | public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { |
这里就不用说了,就是把每一个Resource作为参数,继续调用重载方法。读spring源码,会发现重载方法特别多。
1 | public int loadBeanDefinitions(Resource resource) throws |
还是重载方法,不过这里对传进来的Resource又进行了一次封装,变成了编码后的Resource。
1 | public int loadBeanDefinitions(EncodedResource encodedResource) |
这个就是loadBeanDefinitions()的最后一个重载方法,比较长,可以拆看来看。
1 | Assert.notNull(encodedResource, "EncodedResource must not be null"); |
这第一部分,是处理线程相关的工作,把当前正在解析的Resource,设置为当前Resource。
1 | try { |
这里是第二部分,是核心,首先把Resource还原为InputStream,然后调用实际解析的方法doLoadBeanDefinitions()。可以看到,这种命名方式是很值得学习的,一种业务方法,比如parse(),可能需要做一些外围的工作,然后实际解析的方法,可以命名为doParse()。这种doXXX()的命名方法,在很多开源框架中都有应用,比如logback等。
接下来就看一下这个doLoadBeanDefinitions()方法
1 | protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) |
抛开异常处理:核心代码如下:
1 | Document doc = doLoadDocument(inputSource, resource); |
doLoadDocument方法将InputStream读取成标准的Document对象,然后调用registerBeanDefinitions(),进行解析工作。
1 | protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { |
接下来就看一下这个核心方法registerBeanDefinitions
1 | public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { |
这里注意两点 :
1.Document对象
首先这个Document对象,是W3C定义的标准XML对象,跟spring无关。其次这个registerBeanDefinitions方法,我觉得命名有点误导性。因为这个时候实际上解析还没有开始,怎么直接就注册了呢。比较好的命名,我觉得可以是parseAndRegisterBeanDefinitions()。
2.documentReader的创建时使用反射创建的,代码如下
1 | protected BeanDefinitionDocumentReader |
instantiateClass方法中传入了一个Class类型的参数。追溯发现下述代码:
1 | private Class<?> documentReaderClass = |
所以创建的documentReaderClass是DefaultBeanDefinitionDocumentReader类的实例。
接下来就进入BeanDefinitionDocumentReader 中定义的registerBeanDefinitions()方法看看
1 | public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { |
处理完外围事务之后,进入doRegisterBeanDefinitions()方法,这种命名规范,上文已经介绍过了
1 | protected void doRegisterBeanDefinitions(Element root) { |
这个方法也比较长,拆开来看
1 | String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); |
如果配置文件中元素,配有profile属性,就会进入这一段,不过一般都是不会的
1 | BeanDefinitionParserDelegate parent = this.delegate; |
然后这里创建了BeanDefinitionParserDelegate对象,preProcessXml()和postProcessXml()都是空方法,核心就是parseBeanDefinitions()方法。这里又把BeanDefinition解析和注册的工作,委托给了BeanDefinitionParserDelegate对象,在parseBeanDefinitions()方法中完成
总的来说,解析工作的委托链是这样的:ClassPathXmlApplicationContext,XmlBeanDefinitionReader,DefaultBeanDefinitionDocumentReader,BeanDefinitionParserDelegate
ClassPathXmlApplicationContext作为最外围的组件,发起解析的请求
XmlBeanDefinitionReader将配置文件路径封装为Resource,读取出w3c定义的Document对象,然后委托给DefaultBeanDefinitionDocumentReader
DefaultBeanDefinitionDocumentReader就开始做实际的解析工作了,但是涉及到bean的具体解析,它还是会继续委托给BeanDefinitionParserDelegate来做。
接下来在parseBeanDefinitions()方法中发生了什么,以及BeanDefinitionParserDelegate类完成的工作,在下一篇博客中继续介绍。
loadBeanDefinitions
BeanDefinition的解析,已经走到了DefaultBeanDefinitionDocumentR
eader里,这时候配置文件已经被加载,并解析成w3c的Document对象。这篇博客就接着介绍,DefaultBeanDefinitionDocumentReader和BeanDefinitionParserDelegate类,是怎么协同完成bean的解析和注册的。
1 | BeanDefinitionParserDelegate parent = this.delegate; |
这段代码,创建了一个BeanDefinitionParserDelegate组件,然后就是preProcessXml()、parseBeanDefinitions()、postProcessXml()方法
其中preProcessXml()和postProcessXml()默认是空方法,接下来就看下parseBeanDefinitions()方法
1 | protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { |
从这个方法开始,BeanDefinitionParserDelegate就开始发挥作用了,判断当前解析元素是否属于默认的命名空间,如果是的话,就调用parseDefaultElement()方法,否则调用delegate上parseCustomElement()方法
1 | public boolean isDefaultNamespace(String namespaceUri) { |
只有http://www.springframework.org/schema/beans,会被认为是默认的命名空间。也就是说,beans、bean这些元素,会认为属于默认的命名空间,而像task:scheduled这些,就认为不属于默认命名空间。
根节点beans的一个子节点bean,是属于默认命名空间的,所以会进入parseDefaultElement()方法
1 | private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { |
这里可能会有4种情况,import、alias、bean、beans,分别有一个方法与之对应,这里解析的是bean元素,所以会进入processBeanDefinition()方法
1 | protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { |
这里主要有3个步骤,先是委托delegate对bean进行解析,然后委托delegate对bean进行装饰,最后由一个工具类来完成BeanDefinition的注册
可以看出来,DefaultBeanDefinitionDocumentReader不负责任何具体的bean解析,它面向的是xml Document对象,根据其元素的命名空间和名称,起一个类似路由的作用(不过,命名空间的判断,也是委托给delegate来做的)。所以这个类的命名,是比较贴切的,突出了其面向Document的特性。具体的工作,是由BeanDefinitionParserDelegate来完成的
下面就看下parseBeanDefinitionElement()方法
1 | public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) { |
这个方法很长,可以分成三段来看
1 | String id = ele.getAttribute(ID_ATTRIBUTE); |
这一段,主要是处理一些跟alias,id等标识相关的东西
1 | AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); |
这一行是核心,进行实际的解析
1 | if (beanDefinition != null) { |
这段是后置处理,对beanName进行处理
前置处理和后置处理,不是核心,就不细看了,重点看下核心的那一行调用
1 | public AbstractBeanDefinition parseBeanDefinitionElement( |
这个方法也挺长的,拆开看看
1 | this.parseState.push(new BeanEntry(beanName)); |
这段是从配置中抽取出类名。接下来的长长一段,把异常处理先抛开,看看实际的业务
1 | String parent = null; |
这里每个方法的命名,就说明了是要干什么,可以一个个跟进去看,本文就不细说了。总之,经过这里的解析,就得到了一个完整的BeanDefinitionHolder。只是说明一下,如果在配置文件里,没有对一些属性进行设置,比如autowire-candidate等,那么这个解析生成的BeanDefinition,都会得到一个默认值
然后,对这个Bean做一些必要的装饰
1 | public BeanDefinitionHolder decorateBeanDefinitionIfRequired( |
持续单步调试,代码继续运行到DefaultBeanDefinitionDocumentReader中的processBeanDefinition中的registerBeanDefinition()
1 | BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, |
单步进入代码发现BeanDefinitionReaderUtils静态方法registerBeanDefinition()
1 | public static void registerBeanDefinition( |
解释一下其实调用的是DefaultListableBeanFactory中的registerBeanDefinition方法这句话,因为DefaultListableBeanFactory实现BeanDefinitionRegistry接口,BeanDefinitionRegistry接口中定义了registerBeanDefinition()方法
看下DefaultListableBeanFactory中registerBeanDefinition()实例方法的具体实现:
1 | public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) |
代码追溯之后发现这个方法里,最关键的是以下2行:
1 | this.beanDefinitionNames.add(beanName); |
前者是把beanName放到队列里,后者是把BeanDefinition放到map中,到此注册就完成了。在后面实例化的时候,就是把beanDefinitionMap中的BeanDefinition取出来,逐一实例化
BeanFactory准备完毕之后,代码又回到了ClassPathXmlApplicationContext里
1 | public void refresh() throws BeansException, IllegalStateException { |
也就是obtainFreshBeanFactory()方法执行之后,再进行下面的步骤。
总结来说,ApplicationContext将解析配置文件的工作委托给BeanDefinitionReader,然后BeanDefinitionReader将配置文件读取为xml的Document文档之后,又委托给BeanDefinitionDocumentReader
BeanDefinitionDocumentReader这个组件是根据xml元素的命名空间和元素名,起到一个路由的作用,实际的解析工作,是委托给BeanDefinitionParserDelegate来完成的。
BeanDefinitionParserDelegate的解析工作完成以后,会返回BeanDefinitionHolder给BeanDefinitionDocumentReader,在这里,会委托给DefaultListableBeanFactory完成bean的注册
XmlBeanDefinitionReader(计数、解析XML文档),BeanDefinitionDocumentReader(依赖xml文档,进行解析和注册),BeanDefinitionParserDelegate(实际的解析工作)。
可以看出,在解析bean的过程中,这3个组件的分工是比较清晰的,各司其职,这种设计思想值得学习
到此为止,bean的解析、注册、spring ioc 容器的实例化过程就基本分析结束了。
微信公众号
个人公众号:程序员黄小斜
微信公众号【程序员黄小斜】新生代青年聚集地,程序员成长充电站。作者黄小斜,职业是阿里程序员,身份是斜杠青年,希望和更多的程序员交朋友,一起进步和成长!这一次,我们一起出发。
关注公众号后回复“2019”领取我这两年整理的学习资料,涵盖自学编程、求职面试、算法刷题、Java技术、计算机基础和考研等8000G资料合集。
技术公众号:Java技术江湖
微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。